﻿﻿using System;
﻿using System.Collections.Generic;
﻿using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
﻿using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
﻿using System.Windows.Shell;
﻿using System.Windows.Threading;
﻿using Ookii.Dialogs.Wpf;
﻿using SearchInFiles.Properties;

namespace SearchInFiles
{
	public sealed partial class MainWindow : IDisposable
	{
		#region Fields

		private const string LAST_SETTINGS_FILE_NAME = "last.txt";
		private const string OUTPUT_FILE_NAME = "output.txt";
		private const int MAX_BORDER_LENGTH = 20;
        private const string MATCH_SEPARATOR = "--";
        private const string EXTENSION_SEPARATOR = ".";

		private static readonly char[] Separator = new[] { ';', ',' };

		private readonly Searcher _searcher = new Searcher();

		private readonly VistaFolderBrowserDialog _dialog =
				new VistaFolderBrowserDialog
				{
					ShowNewFolderButton = false,
					Multiselect = true,
					RootFolder = Environment.SpecialFolder.MyComputer
				};

		private readonly Progress<int> _progress = new Progress<int>();
		private CancellationTokenSource _cts = new CancellationTokenSource();
		private bool _fileOutput;
		private readonly StringBuilder _fileOuptputBuffer = new StringBuilder();
		private string _curPath;
        private bool _stopScroll;
        private long _matchNumber;

        private class FormData
        {
            public ICollection<string> AddedExtensions { get; set; }
            public ICollection<string> SearchExpressions { get; set; }
            public ICollection<string> FileTypeFilters { get; set; }
        }

        private FormData FiltersData { get; set; }

	    #endregion

		#region Constructor

		public MainWindow()
		{
            InitializeComponent();
            Loaded += MainWindow_Loaded;
            FiltersData = new FormData();
			LoadSettings();
		    TextBoxSearchExpressions_TextChanged(null,null);
			_searcher.MatchesFound += SearcherMatchesFound;
			_searcher.Error += Searcher_Error;
			_searcher.CheckCancellation = () => _cts.Token.IsCancellationRequested;
			_searcher.ReportProgress = percentage => ((IProgress<int>)_progress).Report(percentage);
			_progress.ProgressChanged += (s, args) =>
            {
                ProgressBarFilesProcessed.ToolTip = Utilities.FormatPlural(_searcher.ProcessedFilesCount, "{0} файл из {1}", "{0} файла из {1}", "{0} файлов из {1}", _searcher.ProcessedFilesCount, _searcher.AllFilesCount);
				ProgressBarFilesProcessed.Value = args;
                TaskbarItemInfo.ProgressValue = (double)args / 100;
			};
		}

	    #endregion

		#region Events
        
        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            foreach (var tb in SearchInFiles.FindVisualChildren<TextBlock>())
            {
                tb.VerticalAlignment = VerticalAlignment.Center;
            }
        }

		private async void ButtonSearch_Click(object sender, RoutedEventArgs e)
		{
			Clear();
			SaveSettings();
            ProgressBarFilesProcessed.ShowControl();
            TaskbarItemInfo.ProgressState = TaskbarItemProgressState.Normal;
		    ChangeState(false, ButtonSearch, ButtonChoose, TextBoxPaths, TextBoxSearchExpressions, CheckBoxIgnoreCase,
                        ComboBoxSearchType, CheckBoxFileOutput, CheckBoxUseRegex, TextBoxFileTypeFilters);
		    TextBoxSearchInResults.HideControl();
			ButtonCancel.IsEnabled = true;
			_stopScroll = false;
			_fileOutput = CheckBoxFileOutput.IsChecked ?? false;
			_fileOuptputBuffer.Clear();
			_curPath = null;
		    _matchNumber = 0;
			var useRegex = CheckBoxUseRegex.IsChecked ?? false;
			var ignoreCase = CheckBoxIgnoreCase.IsChecked ?? false;
            var paths = SplitInput(TextBoxPaths.Text);
            var searchExpressions = SplitInput(TextBoxSearchExpressions.Text);
            var fileTypeFilters = SplitInput(TextBoxFileTypeFilters.Text);
            if (fileTypeFilters != null)
            {
                for (var index = 0; index < fileTypeFilters.Length; index++)
                {
                    fileTypeFilters[index] = ToFileTypeFilter(fileTypeFilters[index]);
                }
            }
		    var searchType = SearchType.NamesAndContent;
			switch (ComboBoxSearchType.SelectedIndex)
			{
				case 0:
					searchType = SearchType.NamesAndContent;
					break;
				case 1: searchType = SearchType.Names;
					break;
				case 2: searchType = SearchType.Content;
					break;
			}
			var sw = new Stopwatch();
			sw.Start();
			await Task.Factory.StartNew(() => _searcher.Process(paths, useRegex, ignoreCase, searchExpressions, fileTypeFilters, searchType));
			sw.Stop();
            ButtonCancel.IsEnabled = false;
            ChangeState(true, ButtonSearch, ButtonChoose, TextBoxPaths, TextBoxSearchExpressions, CheckBoxIgnoreCase,
                        ComboBoxSearchType, CheckBoxFileOutput, CheckBoxUseRegex, TextBoxFileTypeFilters);
            TextBoxSearchExpressions_TextChanged(null, null);
            TaskbarItemInfo.ProgressState = _searcher.State == State.Error?TaskbarItemProgressState.Error:TaskbarItemProgressState.None;
			string msg = null;
			var brsh = Brushes.Black;
			switch (_searcher.State)
			{
				case State.Stopped:
					{
						brsh = Brushes.Red;
						msg = "Прервано пользователем";
						break;
					}
				case State.Success:
					{
						brsh = Brushes.ForestGreen;
						msg = "Готово";
						break;
					}
				case State.NothingFound:
					{
						brsh = Brushes.OrangeRed;
						msg = "Ничего не найдено по запросу '" + TextBoxSearchExpressions.Text + "'";
						break;
					}
				default:
					{
						break;
					}
			}
			if (msg != null)
			{
				AddMessage(new TextBlock
				{
					Text = msg + " (затрачено " + sw.Elapsed.TotalSeconds + " сек.)",
					Foreground = brsh
				});
			}
			HideProgressBar();
			if (_fileOutput && _curPath != null)
			{
				FlushBuffer();
				_curPath = null;
				Process.Start(OUTPUT_FILE_NAME);
			}
		}

		#region Searcher events

        private struct MatchView
        {
            public MatchView(string text, bool isMatch, Extensions.TrimType trimType) : this()
            {
                Text = text;
                IsMatch = isMatch;
                TrimType = trimType;
            }

            public string Text { get; private set; }
            public bool IsMatch { get; private set; }
            public Extensions.TrimType TrimType { get; private set; }
        }

	    private void SearcherMatchesFound(object sender, MatchesFoundEventArgs e)
	    {
	        var path = e.Matches.Path;
	        var lineNumber = e.Matches.LineNumber;
	        var matchedText = e.Matches.MatchedText;
	        var matchType = e.Matches.MatchType;
	        if (path != _curPath)
	        {
	            _curPath = path;
	            //Добавляем имя файла, если соответствие найдено в содержимом
	            if (matchType == MatchType.Content)
	            {
	                if (_fileOutput)
	                {
	                    _fileOuptputBuffer.AppendLine(path);
	                }
	                else
	                {
	                    ListBoxMessages.Dispatcher.Invoke((() => AddMessage(new Message(path, null, path, null, matchType))), DispatcherPriority.Background);
	                }
	            }
	        }
	        var matchNumbers = string.Join(", ", e.Matches.Matches.Select(m => "#" + (_matchNumber++ + 1))) + ": ";
	        var indexes = string.Join(", ", e.Matches.Matches.Select(m => m.Index + 1));

	        var views = new List<MatchView>();
	        string headerView;
	        if (matchType == MatchType.Content)
	        {
	            headerView = "\t" + matchNumbers + string.Format("Стр. {0}, поз. {1}: ", lineNumber + 1, indexes);
	        }
	        else
	        {
	            headerView=matchNumbers;
                views.Add(new MatchView(Path.GetDirectoryName(path) + "\\", false, Extensions.TrimType.NoTrimNoCollapse));
	        }
	        var prevEndIndex = 0;
	        var i = 0;
	        foreach (var match in e.Matches.Matches)
	        {
	            var matchIndex = match.Index;
	            var matchLength = match.Length;
	            var startIndex = prevEndIndex;
	            if (startIndex < match.Index)
	            {
	                var pre = matchedText.Substring(startIndex, matchIndex - startIndex);
	                views.Add(new MatchView(pre, false, i == 0 ? Extensions.TrimType.TrimStart : Extensions.TrimType.NoTrim));
	            }
                //Если поисковый текст накладывается сам на себя, например, ищем aa в ааа
                else if (startIndex > matchIndex)
                {
                    matchLength -= startIndex - matchIndex;
                    if (matchLength <= 0)
                    {
                        continue;
                    }
                    matchIndex = startIndex;
                }
	            var exact = matchedText.Substring(matchIndex, matchLength);
	            if (_fileOutput)
	            {
	                exact = MATCH_SEPARATOR + exact + MATCH_SEPARATOR;
	            }
	            views.Add(new MatchView(exact, true, Extensions.TrimType.NoTrimNoCollapse));
	            prevEndIndex = matchIndex + matchLength;
	            i++;
	        }

	        if (prevEndIndex < matchedText.Length)
	            views.Add(new MatchView(matchedText.Substring(prevEndIndex), false, Extensions.TrimType.TrimEnd));
	        if (_fileOutput)
	        {
	            _fileOuptputBuffer.AppendLine(headerView +
	                                          string.Join(null,
	                                                      views.Select(v => v.Text.Collapse(MAX_BORDER_LENGTH, v.TrimType))));
	        }
	        else
	        {
	            try
	            {
	                ListBoxMessages.Dispatcher.Invoke((() =>
	                    {
                            var message = new Message(path, lineNumber, null, path, matchType);
	                        message.Inlines.Add(
	                            new Run(headerView)
	                                {
	                                    FontWeight = FontWeights.Bold
	                                });
	                        message.Inlines.AddRange(
	                            views.Select(
	                                v =>
	                                v.IsMatch
	                                    ? new Run(v.Text)
	                                        {
	                                            Foreground = Brushes.ForestGreen,
	                                            FontWeight = FontWeights.Bold
	                                        }
	                                    : new Run(v.Text.Collapse(MAX_BORDER_LENGTH, v.TrimType))));
	                        AddMessage(message);
	                    }), DispatcherPriority.Background);
	            }
	            catch (TaskCanceledException)
	            {

	            }
	        }
	    }

	    private void Searcher_Error(object sender, ErrorEventArgs e)
		{
			ListBoxMessages.Dispatcher.Invoke(
					(() =>
					{
						ListBoxMessages.Items.Add(new Label
						{
							Content = new TextBlock { Text = e.Message, Foreground = Brushes.Red },
							ToolTip = "Ошибка!"
						});
						ListBoxMessages.ScrollIntoView(ListBoxMessages.Items[ListBoxMessages.Items.Count - 1]);
					}
                    ), DispatcherPriority.Background);
		}

		#endregion

		private void ButtonChoose_Click(object sender, RoutedEventArgs e)
		{
			var path = TextBoxPaths.Text.Split(';')[0];
			if (Directory.Exists(path))
			{
				_dialog.SelectedPath = path;
			}
			if (_dialog.ShowDialog(this) == true)
			{
				ButtonSearch.IsEnabled = true;
				TextBoxPaths.Text = string.Join("; ", _dialog.SelectedPaths);
			}
			Clear();
		}

		private void TextBoxDirectoryPath_TextChanged(object sender, TextChangedEventArgs e)
		{
			ButtonSearch.IsEnabled = !string.IsNullOrEmpty(TextBoxPaths.Text);
		}

		private void ButtonCancel_Click(object sender, RoutedEventArgs e)
		{
			_cts.Cancel();
			HideProgressBar();
		}

		private void ListBoxMessagesItem_MouseDoubleClick(object sender, MouseButtonEventArgs e)
		{
			var item = sender as ListBoxItem;
			if (item == null) return;
			var label = item.Content as Message;
			if (label != null)
			{
				label.OpenDefault();
			}
		}

	    private void TextBoxSearchInResults_KeyDown(object sender, KeyEventArgs e)
	    {
	        switch (e.Key)
	        {
	            case Key.Return:
	                SearchInResults();
	                e.Handled = true;
	                break;
	            case Key.Escape:
	                TextBoxSearchInResults.Text = string.Empty;
	                SearchInResults();
	                e.Handled = true;
	                break;
	        }
	    }

	    private void Window_KeyDown(object sender, KeyEventArgs e)
	    {
	        if (e.Key == Key.Return)
	        {
                if (BorderApplyFiltersFromSearchExpressions.Visibility == Visibility.Visible)
	            {
	                ButtonApplyFiltersFromSearchExpressions_Click(null, null);
	            }
	            else
	            {
                    if (ButtonSearch.IsEnabled)
                    {
                        ButtonSearch_Click(null, null);
                    }
	            }
	        }
	        else if (e.Key == Key.F && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control && TextBoxSearchInResults.Visibility == Visibility.Visible)
	        {
	            TextBoxSearchInResults.Focus();
	        }
	    }

	    private void ListBoxMessages_SelectionChanged(object sender, SelectionChangedEventArgs e)
		{
			if (e.AddedItems.Count > 0)
			{
				_stopScroll = true;
				ListBoxMessages.ScrollIntoView(e.AddedItems[0]);
			}
			else
			{
				_stopScroll = false;
			}
		}

	    private void TextBoxSearchExpressions_TextChanged(object sender, TextChangedEventArgs e)
	    {
	        var searchExpressions = SplitInput(TextBoxSearchExpressions.Text);
	        ComboBoxSearchType.IsEnabled = (searchExpressions != null);
	        ApplyFiltersFromSearchExpressions(searchExpressions);
	    }

        private void TextBoxFileTypeFilters_TextChanged(object sender, TextChangedEventArgs e)
        {
            ApplyFiltersFromSearchExpressions(SplitInput(TextBoxSearchExpressions.Text));
        }

	    private void TextBoxSearchInResults_TextChanged(object sender, TextChangedEventArgs e)
	    {
	        SearchInResults();
	    }

        private void ListBoxMessages_PreviewMouseDown(object sender, MouseButtonEventArgs e)
        {
            _stopScroll = true;
        }

	    private void Window_PreviewMouseUp(object sender, MouseButtonEventArgs e)
	    {
            //Если нужно, чтобы скролл продолжался после того, как отпустили кнопку мыши, то раскомментировать
            //if (ListBoxMessages.SelectedIndex == -1)
            //{
            //    _stopScroll = false;
            //}
	    }

	    private void ButtonApplyFiltersFromSearchExpressions_Click(object sender, RoutedEventArgs e)
	    {
	        //Если хотя бы один из поисковых запросов похож на имя файла с расширением, то предложим пользователю искать в таких файлах

	        if (!FiltersData.AddedExtensions.Any())
	        {
	            return;
	        }
	        var separator = Separator[1] + " ";
	        TextBoxFileTypeFilters.Text = string.Join(separator, FiltersData.FileTypeFilters);
	        TextBoxSearchExpressions.Text = string.Join(separator, FiltersData.SearchExpressions);
	    }

	    private void SearchInFiles_Closing(object sender, System.ComponentModel.CancelEventArgs e)
		{
			//Чтобы прервать выполнение и генерацию евентов
			_cts.Cancel();
		}
		#endregion

		#region Private

		private void Clear()
		{
			_cts = new CancellationTokenSource();
			ListBoxMessages.Items.Clear();
			HideProgressBar();
		}

		private void FlushBuffer()
		{
			try
			{
				File.WriteAllText(OUTPUT_FILE_NAME, _fileOuptputBuffer.ToString());
				_fileOuptputBuffer.Clear();
			}
			catch
			{
				AddMessage(new TextBlock { Text = "Ошбика при записи файла с результатами", Foreground = Brushes.Red });
			}
		}

		private void SaveSettings()
		{
			try
			{
				var sb = new StringBuilder();
				sb.AppendLine(TextBoxPaths.Text);
				sb.AppendLine(TextBoxSearchExpressions.Text);
				sb.AppendLine(TextBoxFileTypeFilters.Text);
				sb.AppendLine(CheckBoxFileOutput.IsChecked.ToString());
				sb.AppendLine(CheckBoxIgnoreCase.IsChecked.ToString());
				sb.AppendLine(CheckBoxUseRegex.IsChecked.ToString());
				sb.AppendLine(ComboBoxSearchType.SelectedIndex.ToString());
				File.WriteAllText(LAST_SETTINGS_FILE_NAME, sb.ToString());
			}
			catch
			{
                AddMessage(new TextBlock { Text = ErrorResources.CannotSaveSettings, Foreground = Brushes.Red });
			}
		}

		private void LoadSettings()
		{
			if (File.Exists(LAST_SETTINGS_FILE_NAME))
			{
				try
				{
					var lines = File.ReadAllLines(LAST_SETTINGS_FILE_NAME);
					if (lines.Length > 0)
						TextBoxPaths.Text = lines[0];
					if (lines.Length > 1)
						TextBoxSearchExpressions.Text = lines[1];
					if (lines.Length > 2)
						TextBoxFileTypeFilters.Text = lines[2];
					if (lines.Length > 3)
						CheckBoxFileOutput.IsChecked = Convert.ToBoolean(lines[3]);
					if (lines.Length > 4)
						CheckBoxIgnoreCase.IsChecked = Convert.ToBoolean(lines[4]);
					if (lines.Length > 5)
                        CheckBoxUseRegex.IsChecked = Convert.ToBoolean(lines[5]);
                    if (lines.Length > 6)
                        ComboBoxSearchType.SelectedIndex = Convert.ToInt32(lines[6]);
				}
				catch
				{
                    AddMessage(new TextBlock { Text = ErrorResources.CannotLoadSettings, Foreground = Brushes.Red });
				}
			}
		}

		private void HideProgressBar()
		{
			ProgressBarFilesProcessed.Value = 0;
			ProgressBarFilesProcessed.HideControl();
		}

		private void AddMessage(TextBlock content)
		{
		    if (content is Message)
		    {
		        TextBoxSearchInResults.ShowControl();
		    }
		    ListBoxMessages.Items.Add(content);
		    SearchInResult(content);
			if (!_stopScroll)
			{
				ListBoxMessages.ScrollIntoView(ListBoxMessages.Items[ListBoxMessages.Items.Count - 1]);
			}
		}

	    private static void ChangeState(bool enable, params Control[] controls)
	    {
	        foreach (var control in controls)
	        {
	            control.IsEnabled = enable;
	        }
	    }

	    private static string[] SplitInput(string input)
	    {
	        return string.IsNullOrWhiteSpace(input)
	                   ? null
	                   : input.Split(Separator, StringSplitOptions.RemoveEmptyEntries)
	                          .Where(x => !string.IsNullOrWhiteSpace(x))
	                          .Select(x => x.Trim())
                              .ToArray()
	                          .NullIfEmpty();
	    }

        private static string ToFileTypeFilter(string str)
        {
            return !str.StartsWith("*.") ? "*." + str : str;
        }

        private void SearchInResults()
        {
            var searchText = TextBoxSearchInResults.Text;
            var isEmpty = string.IsNullOrWhiteSpace(searchText);
            if (isEmpty)
            {
                _stopScroll = false;
            }
            foreach (var message in ListBoxMessages.Items.OfType<Message>().AsParallel())
            {
                if (SearchInResult(message))
                {
                    _stopScroll = true;
                    ListBoxMessages.ScrollIntoView(message);
                }
                else
                {
                    message.Background = null;
                }
            }
        }

	    private bool SearchInResult(TextBlock message)
	    {
	        var searchText = TextBoxSearchInResults.Text;
	        var isEmpty = string.IsNullOrWhiteSpace(searchText);
	        if (!isEmpty)
	        {
	            var text = string.Join("", message.Inlines.OfType<Run>().Select(x => x.Text)).ToLower();
	            if (text.Contains(searchText.ToLower()))
	            {
	                message.Background = Brushes.Yellow;
	                return true;
	            }
	        }
	        return false;
	    }

	    private void ApplyFiltersFromSearchExpressions(IEnumerable<string> searchExpressions)
	    {
	        var fileTypeFilters = SplitInput(TextBoxFileTypeFilters.Text);
	        FiltersData.SearchExpressions = new List<string>();
	        FiltersData.FileTypeFilters = fileTypeFilters == null ? new HashSet<string>() : new HashSet<string>(fileTypeFilters.Select(ToFileTypeFilter));
	        FiltersData.AddedExtensions = new List<string>();
	        if (searchExpressions != null)
	        {
	            foreach (var s in searchExpressions)
	            {
	                var parts = s.Split(EXTENSION_SEPARATOR[0]).ToList();
	                if (parts.Count > 1)
	                {
	                    FiltersData.SearchExpressions.Add(parts[0]);
	                    parts.RemoveAt(0);
	                    parts = parts.Where(p => !string.IsNullOrWhiteSpace(p)).ToList();
	                    if (parts.Any())
	                    {
	                        var extension = ToFileTypeFilter(string.Join(EXTENSION_SEPARATOR, parts));
	                        if (!FiltersData.FileTypeFilters.Contains(extension))
	                        {
	                            FiltersData.AddedExtensions.Add(extension);
	                            FiltersData.FileTypeFilters.Add(extension);
	                        }
	                    }
	                }
	                else
	                {
	                    FiltersData.SearchExpressions.Add(s);
	                }
	            }
	            var separator = Separator[1] + " ";
	            TextBlockApplyFiltersFromSearchExpressions.Text = String.Format(GuiResources.TextBlockApplyFiltersFromSearchExpressionsContent,
	                                                                            string.Join(separator, FiltersData.AddedExtensions));
	        }
	        BorderApplyFiltersFromSearchExpressions.ToggleControl(!FiltersData.AddedExtensions.Any());
	    }

	    #endregion

        #region IDisposable
        private bool _disposed;

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

	    private void Dispose(bool disposing)
	    {
	        if (_disposed) return;
	        if (disposing)
	        {
	            _cts.Dispose();
	        }
	        _disposed = true;
        }
        #endregion
    }
}